/*
 *  SyncIT , easy synchronization of files between computers and devices.
 *  Copyright (C) 2011  Christian Masus <cmasus(at)gmail.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version. If redistributing and/or modifying
 *  this file, this notice must be included in the redistributed or
 *  modified copy.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package config;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;

import config.items.GeneralItems;
import config.items.GuiItems;
import config.items.LogItems;
import config.items.SyncItems;

/**
 * Interface for reading and writing configuration values.
 * The configuration will also be stored permanently.
 * @author Christian Masus
 *
 */
public class Config implements ConfigInterface {

	private final String TYPE_ELEMENT = "element";
	private final String TYPE_LIST = "list";

	private ArrayList<ConfigListItem> listItem;
	private PermanentStorage storage;

	public Config() {
		listItem = new ArrayList<ConfigListItem>();
		storage = new PermanentStorage();
		init();
	}

	public Config(String configPath) {
		listItem = new ArrayList<ConfigListItem>();
		if(configPath != null && !configPath.equals("")) {
			storage = new PermanentStorage(configPath);			
		}
		else {
			storage = new PermanentStorage();
		}
		init();
	}

	private void init() {
		if(storage.init()) {
			String[] configStrs = storage.loadConfig();
			addItems(false);
			parseAndLoadConfig(configStrs);
		}
		else {
			addItems(true);
		}
	}

	private void addItems(boolean save) {
		addElement("", GeneralItems.class.getName(), save);
		addList("client", SyncItems.class.getName());
		addElement("log", LogItems.class.getName(), save);
		addElement("gui", GuiItems.class.getName(), save);
	}


	public String getConfigPath() {
		return storage.getConfigPath();
	}
	public void setConfigPath(String configPath) {
		storage.setConfigPath(configPath);
	}



	public boolean addElement(String path, String className) {
		return addElement(path, className, true);
	}
	public boolean addElement(String path, String className, boolean save) {
		try {
			Class<?> c = Class.forName(className);
			Object o = c.newInstance();
			listItem.add( new ConfigListItem(path, TYPE_ELEMENT, c, o) );
			if(save) return storeConfig();
			else     return true;
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
		return false;
	}

	public boolean addList(String path, String className) {
		try {
			Class<?> c = Class.forName(className);
			listItem.add( new ConfigListItem(path, TYPE_LIST, c, null) );
			return true;
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		return false;
	}

	/**
	 * Adds a list element to the end of the list. Also saves the information in permanent storage.
	 * @param path the path that identifies the list.
	 * @return the new element index, or -1 if an error occurs.
	 */
	public int addListElement(String path) {
		return addListElement(path, true);
	}
	public int addListElement(String path, boolean save) {
		int index = getListItemIndexByName(path);
		ConfigListItem item = listItem.get(index);
		Class<?> c = item.getC();
		try {
			Object o = c.newInstance();
			item.getList().add( new ConfigListItem(path, TYPE_ELEMENT, c, o) );
			if(save) {
				if(storeConfig()) {
					return item.getList().size()-1;
				}
				else {
					return -1;
				}
			}
			else {
				return item.getList().size()-1;
			}
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
		return -1;
	}

	/**
	 * Removes an element from the list.
	 * @param path the path that identifies the list.
	 * @param listIndex the index to be deleted in the list.
	 * @return true if changes where successfully made and saved permanently, otherwise false.
	 */
	public boolean deleteListElement(String path, int listIndex) {
		return deleteListElement(path, listIndex, true);
	}
	public boolean deleteListElement(String path, int listIndex, boolean save) {
		int index = getListItemIndexByName(path);
		ArrayList<ConfigListItem> list = listItem.get(index).getList();
		list.remove(listIndex);
		if(save) return storeConfig();
		else     return true;
	}

	/**
	 * Switches places between two elements in the list.
	 * @param path the path that identifies the list.
	 * @param from index move location.
	 * @param to move from to this index.
	 * @return true if changes where successfully made and saved permanently, otherwise false.
	 */
	public boolean moveListElement(String path, int from, int to) {
		return moveListElement(path, from, to, true);
	}
	public boolean moveListElement(String path, int from, int to, boolean save) {
		int index = getListItemIndexByName(path);
		ArrayList<ConfigListItem> list = listItem.get(index).getList();
		int listSize = list.size();
		if(from < 0 || to < 0 || from >= listSize || to >= listSize) { // Return if anything is out of range
			return false;
		}
		ConfigListItem item = list.get(from);
		list.remove(from);
		list.add(to, item);
		if(save) return storeConfig();
		else     return true;
	}

	private int getListItemIndexByName(String path) {
		for(int i = 0; i < listItem.size(); i++) {
			if( listItem.get(i).getPath().equals(path) ) {
				return i;
			}
		}
		return -1;
	}






	public boolean setVariable(String path, String name, Object value) {
		return setVariable(path, name, value, true);
	}
	public boolean setVariable(String path, String name, Object value, boolean save) {
		int index = getListItemIndexByName(path);
		ConfigListItem item = null;
		try {
			item = listItem.get(index);
		} catch (ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			return false;
		}
		String type = item.getType();

		if(type.equals(TYPE_ELEMENT)) {
			Class<?> c = item.getC();
			Object o = item.getO();
			try {
				Method m;
				if(value instanceof Integer) {
					m = c.getMethod("set" + name, int.class);
				}
				else if(value instanceof Boolean) {
					m = c.getMethod("set" + name, boolean.class);
				}
				else { // if(value instanceof String)
					m = c.getMethod("set" + name, String.class);
				}
				m.invoke(o, value);
				if(save) return storeConfig();
				else     return true;
			} catch (SecurityException e) {
				e.printStackTrace();
			} catch (NoSuchMethodException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}
		return false;
	}

	public Object getVariable(String path, String name) {
		int index = getListItemIndexByName(path);
		ConfigListItem item = null;
		try {
			item = listItem.get(index);
		} catch (ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			return false;
		}
		String type = item.getType();

		if(type.equals(TYPE_ELEMENT)) {
			Class<?> c = item.getC();
			Object o = item.getO();
			try {
				Method m = c.getMethod("get" + name);
				return m.invoke(o);
			} catch (SecurityException e) {
				e.printStackTrace();
			} catch (NoSuchMethodException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}
		return null;
	}

	public String getVariableString(String path, String name) {
		return (String) getVariable(path, name);
	}
	public int getVariableInteger(String path, String name) {
		return (Integer) getVariable(path, name);
	}
	public boolean getVariableBoolean(String path, String name) {
		return (Boolean) getVariable(path, name);
	}



	public boolean setListVariable(String pathIndex, String name, Object value, boolean save) {
		int lastIx = pathIndex.lastIndexOf("/");
		String path = pathIndex.substring(0, lastIx);
		int listIndex = new Integer( pathIndex.substring(lastIx+1) );
		return setListVariable(path, listIndex, name, value, save);
	}
	public boolean setListVariable(String path, int listIndex, String name, Object value) {
		return setListVariable(path, listIndex, name, value, true);
	}
	public boolean setListVariable(String path, int listIndex, String name, Object value, boolean save) {
		int index = getListItemIndexByName(path);
		ConfigListItem item = null;
		try {
			item = listItem.get(index);
		} catch (ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			return false;
		}
		String type = item.getType();
		ConfigListItem listItem = null;
		try {
			listItem = item.getList().get(listIndex);
		} catch (ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			return false;
		}

		if(type.equals(TYPE_LIST)) {
			Class<?> c = listItem.getC();
			Object o = listItem.getO();
			try {
				Method m;
				if(value instanceof Integer) {
					m = c.getMethod("set" + name, int.class);
				}
				else if(value instanceof Boolean) {
					m = c.getMethod("set" + name, boolean.class);
				}
				else { // if(value instanceof String)
					m = c.getMethod("set" + name, String.class);
				}
				m.invoke(o, value);
				if(save) return storeConfig();
				else     return true;
			} catch (SecurityException e) {
				e.printStackTrace();
			} catch (NoSuchMethodException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}
		return false;
	}

	public Object getListVariable(String path, int listIndex, String name) {
		int index = getListItemIndexByName(path);
		ConfigListItem item = null;
		try {
			item = listItem.get(index);
		} catch (ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			return false;
		}
		String type = item.getType();
		ConfigListItem listItem = null;
		try {
			listItem = item.getList().get(listIndex);
		} catch (ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			return false;
		}

		if(type.equals(TYPE_LIST)) {
			Class<?> c = listItem.getC();
			Object o = listItem.getO();
			try {
				Method m = c.getMethod("get" + name);
				return m.invoke(o);
			} catch (SecurityException e) {
				e.printStackTrace();
			} catch (NoSuchMethodException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}
		return null;
	}

	public String getListVariableString(String path, int listIndex, String name) {
		return (String) getListVariable(path, listIndex, name);
	}
	public int getListVariableInteger(String path, int listIndex, String name) {
		return (Integer) getListVariable(path, listIndex, name);
	}
	public boolean getListVariableBoolean(String path, int listIndex, String name) {
		return (Boolean) getListVariable(path, listIndex, name);
	}

	public int getListSize(String path) {
		int index = getListItemIndexByName(path);
		ConfigListItem item = null;
		try {
			item = listItem.get(index);
		} catch (ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			return -1;
		}
		String type = item.getType();
		if(type.equals(TYPE_LIST)) {
			return item.getList().size();
		}
		return -1;
	}




	public boolean storeConfig() {
		String settingStr = "";
		settingStr += "# SyncIT configuration file.\r\n";
		settingStr += "# Do not edit while program is running.\r\n\r\n";
		try {
			for(int i = 0; i < listItem.size(); i++) {
				ConfigListItem item = listItem.get(i);
				String type = item.getType();
				String path = item.getPath();
				if(type.equals(TYPE_ELEMENT)) {
					Class<?> c = item.getC();
					Object o = item.getO();
					Method m = c.getMethod("getTagValuePair");
					String[][] tagValuePair = (String[][]) m.invoke(o);
					settingStr += convertTagValuesToString(path, tagValuePair);
				}
				else if(type.equals(TYPE_LIST)) {
					ArrayList<ConfigListItem> listList = item.getList();
					for(int j = 0; j < listList.size(); j++) {
						ConfigListItem listItem = listList.get(j);
						String listPath = listItem.getPath();
						Class<?> c = listItem.getC();
						Object o = listItem.getO();
						Method m = c.getMethod("getTagValuePair");
						String[][] tagValuePair = (String[][]) m.invoke(o);
						String addListStr = "add=" + listPath + "/" + j + "\r\n"; // Adding info that a new list element is added
						settingStr += addListStr + convertTagValuesToString(listPath, j, tagValuePair);
					}
				}
			}
			return storage.storeConfig(settingStr);
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
		return false;
	}

	private String convertTagValuesToString(String path, String[][] tagValuePair) {
		String ret = "";
		for(int row = 0; row < tagValuePair.length; row++) {
			ret += path + "/" + tagValuePair[row][0] + "=" + tagValuePair[row][1] + "\r\n";
		}
		return ret;
	}

	private String convertTagValuesToString(String path, int listIndex, String[][] tagValuePair) {
		String ret = "";
		for(int row = 0; row < tagValuePair.length; row++) {
			ret += path + "/" + listIndex + "/" + tagValuePair[row][0] + "=" + tagValuePair[row][1] + "\r\n";
		}
		return ret;
	}

	private boolean parseAndLoadConfig(String[] configStrs) {
		String currentPathIndex = null;
		for(String configStr : configStrs) {
			String[] tagValuePair = configStr.split("=");
			String tag = tagValuePair[0].trim();
			String value = "";
			if(tagValuePair.length == 2) {
				value = tagValuePair[1].trim();
			}

			if(tag.startsWith("#") || tag.equals("")) {
				// Do nothing, this is a comment
			}
			else if(tag.equals("add")) { // Starting of new list item
				currentPathIndex = value;
				int lastIx = value.lastIndexOf("/");
				String currentPath = value.substring(0, lastIx);
				addListElement(currentPath, false);
			}
			else if(currentPathIndex == null || !tag.startsWith(currentPathIndex)) { // Normal config value (not in a list)
				currentPathIndex = null;
				String path;
				String tagName;
				if(!tag.startsWith("/")) {
					int firstIx = tag.indexOf("/");
					if(firstIx != -1) {
						path = tag.substring(0, firstIx);
						tagName = tag.substring(firstIx+1);
						setVariable(path, tagName, value, false);
					}
					else {
						System.err.println("Config: configuration file error on line=" + configStr + " (ignoring)");
					}
				}
				else {
					path = "";
					tagName = tag.substring(1);
					setVariable(path, tagName, value, false);
				} 
			}
			else if(currentPathIndex != null && tag.startsWith(currentPathIndex)) { // If next item in the same list index
				int lastIx = tag.lastIndexOf("/");
				String tagName = tag.substring(lastIx+1);
				setListVariable(currentPathIndex, tagName, value, false);
			}
		}
		return true;
	}




	public String toString() {
		String ret = "";
		for(ConfigListItem item : listItem) {
			String type = item.getType();
			String path = item.getPath();
			if(type.equals(TYPE_ELEMENT)) {
				ret += "Path: " + path;
				ret += getVariableString(path, "toString");
			}
			else { // TYPE_LIST
				ArrayList<ConfigListItem> itemList = item.getList();
				for(int i = 0; i < itemList.size(); i++) {
					ret += "Path: " + path + "/" + i;
					ret += getListVariableString(path, i, "toString");
				}
			}
		}
		return ret;
	}



	private class ConfigListItem {
		private String path;
		private String type;
		private Class<?> c;
		private Object o;
		private ArrayList<ConfigListItem> list;

		public ConfigListItem(String path, String type, Class<?> c, Object o) {
			this.path = path;
			this.type = type;
			this.c = c;
			this.o = o;
			if(type.equals(TYPE_LIST)) {
				list = new ArrayList<ConfigListItem>();
			}
			else {
				list = null;
			}
		}

		public String getPath() {
			return path;
		}

		public String getType() {
			return type;
		}

		public Class<?> getC() {
			return c;
		}

		public Object getO() {
			return o;
		}

		public ArrayList<ConfigListItem> getList() {
			return list;
		}
	}
}
